﻿/*
    Forza Studio: Forza Motorsport resource extraction tool.

    Copyright (C) Mike Davis (http://codeescape.com/)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    A copy of this license can be found in the root of the project
    directory or online at <http://www.gnu.org/licenses/gpl.txt>.
*/

using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Drawing;
using System.Drawing.Imaging;

namespace ForzaStudio
{
    // forza 2 and 3
    public class ForzaTexture : IDisposable
    {
        #region Imports
        [DllImport("xds.dll", EntryPoint = "GetTextureSize")]
        private unsafe static extern void GetTextureSize(byte* inputData, int inputSize, out int outputWidth, out int outputHeight);

        [DllImport("xds.dll", EntryPoint = "Create")]
        private unsafe static extern void Create(byte* inputData, int inputSize, void* outputData);

        [DllImport("xds.dll", EntryPoint = "ConvertToBitmap")]
        private unsafe static extern bool ConvertToBitmap(byte* inputData, int inputSize, int d3dFormat, int width, int height, void* outputData);
        #endregion

        #region Properties
        public Image Image
        {
            get;
            private set;
        }
        #endregion

        #region Constructors / Destructor
        // loads a texture from a zipped file
        public ForzaTexture(XCompress zip, string name)
        {
            // bad argument check
            if (zip == null || name == null || name.Length < 1)
                throw new ArgumentException();

            // make sure name is in all lower case
            name = name.ToLower();

            // cannot load b bix directly, go through main bix file instead
            if (name.EndsWith("_b.bix"))
            {
                name = name.Replace("_b.bix", ".bix");
            }

            foreach (ZipDirEntry entry in zip.Entries)
            {
                if (entry.FileName.Equals(name, StringComparison.InvariantCultureIgnoreCase))
                {
                    if (name.EndsWith(".bix"))
                    {
                        // read the 28 byte header
                        uint width, height, levels, format, totalSize, mainSize;
                        using (MemoryStream ms = new MemoryStream(entry.Data))
                        using (BinaryReader br = new BinaryReader(ms))
                        {
                            if (br.ReadUInt32() != 0x30584942)  // 'BIX0'
                            {
                                throw new Exception("Invalid bix file format.");
                            }
                            width = ReverseEndian(br.ReadUInt32());
                            height = ReverseEndian(br.ReadUInt32());
                            levels = ReverseEndian(br.ReadUInt32());    // not confirmed...
                            format = ReverseEndian(br.ReadUInt32());  // D3DFMT
                            totalSize = ReverseEndian(br.ReadUInt32()); // bix a and b data size
                            mainSize = ReverseEndian(br.ReadUInt32());  // bix b data size
                        }

                        // check for bix b data file
                        ZipDirEntry bixB = zip[name.Replace(".bix", "_b.bix")];
                        if (bixB != null)
                        {
                            LoadBix(bixB.Data, (int)width, (int)height, (int)format);
                        }
                        else throw new Exception("Unable to locate bix texture information.");
                    }
                    else if (name.EndsWith(".xds"))
                    {
                        using (MemoryStream ms = new MemoryStream(entry.Data))
                        {
                            LoadXds(ms);
                        }
                    }
                    else throw new Exception("Unknown file extension.");

                    return;
                }
            }
            throw new Exception("Texture not found.");
        }

        public ForzaTexture(string strFileName)
        {
            // bad filename check
            if (strFileName == null || strFileName.Length < 1)
                throw new ArgumentException();

            // make sure name is in all lower case
            strFileName = strFileName.ToLower();

            if (strFileName.EndsWith(".xds"))
            {
                using (FileStream fs = new FileStream(strFileName, FileMode.Open, FileAccess.Read, FileShare.Read))
                {
                    LoadXds(fs);
                }
            }
            else if (strFileName.EndsWith(".bix"))
            {
                // cannot load b bix directly, go through main bix file instead
                if (strFileName.EndsWith("_b.bix"))
                {
                    strFileName = strFileName.Replace("_b.bix", ".bix");
                }

                // read the 28 byte header
                uint width, height, levels, format, totalSize, mainSize;
                using (FileStream fs = new FileStream(strFileName, FileMode.Open, FileAccess.Read, FileShare.Read))
                using (BinaryReader br = new BinaryReader(fs))
                {
                    if (br.ReadUInt32() != 0x30584942)  // 'BIX0'
                    {
                        throw new Exception("Invalid bix file format.");
                    }
                    width = ReverseEndian(br.ReadUInt32());
                    height = ReverseEndian(br.ReadUInt32());
                    levels = ReverseEndian(br.ReadUInt32());    // not confirmed...
                    format = ReverseEndian(br.ReadUInt32());  // D3DFMT
                    totalSize = ReverseEndian(br.ReadUInt32()); // bix a and b data size
                    mainSize = ReverseEndian(br.ReadUInt32());  // bix b data size
                }

                // check for bix b data file
                using (FileStream fs = new FileStream(strFileName.Replace(".bix", "_b.bix"), FileMode.Open, FileAccess.Read, FileShare.Read))
                {
                    byte[] data = new byte[fs.Length];
                    fs.Read(data, 0, data.Length);
                    LoadBix(data, (int)width, (int)height, (int)format);
                }
            }
            else throw new Exception("Unknown file extension.");
        }

        ~ForzaTexture()
        {
            Dispose();
        }

        #endregion

        #region Methods
        private unsafe void LoadXds(Stream stream)
        {
            int xdsSize = (int)stream.Length;

            // allocate managed byte array large enough to contain aligned xds file
            byte[] xds = new byte[xdsSize + 0x1000 * 2];

            // load xds data
            fixed (byte* xdsPtr = xds)
            {
                // pointer to byte array
                uint basePtr = new UIntPtr(xdsPtr).ToUInt32();

                // offset of 4kb aligned location inside of byte array
                byte* alignedPtr = (byte*)new UIntPtr((basePtr + 0xFFF) & 0xFFFFF000).ToPointer();

                // offset of 4kb aligned location inside of byte array
                int alignedOffset = (int)(new UIntPtr(alignedPtr).ToUInt32() - basePtr);

                // streaming access to managed byte array
                using (MemoryStream ms = new MemoryStream(xds))
                using (BinaryWriter bw = new BinaryWriter(ms))
                {
                    ms.Position = alignedOffset;   // skip to aligned location

                    // construct the first part of the header in big endian format
                    bw.Write((int)0x01000000);
                    bw.Write((int)0x44325854);
                    bw.Write((int)0x30000000);
                    bw.Write((int)0x34000000);
                    bw.Write((int)0x14000000);
                    bw.Write(new byte[28]);

                    // read the other half of the header
                    stream.Read(xds, alignedOffset + 48, 52);

                    // check for valid xds file, although theres no guarantee that another file type won't have these values
                    if (BitConverter.ToInt32(xds, alignedOffset + 48) != 0x03000000 || BitConverter.ToInt32(xds, alignedOffset + 52) != 0x01000000)
                    {
                        throw new Exception("Not a valid XDS file");
                    }

                    // must align texture data on 4kb boundaries
                    stream.Read(xds, alignedOffset + 4096, xdsSize - 52);

                    // pass pre-aligned xds data into xds.dll and get tga bytes
                    int width, height;
                    GetTextureSize(alignedPtr, xdsSize, out width, out height);

                    Bitmap bmp = null;
                    try
                    {
                        // create a new bitmap and lock its position in memory
                        bmp = new Bitmap(width, height, PixelFormat.Format32bppArgb);
                        BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, bmp.PixelFormat);

                        // convert the texture and save to the bitmap in memory
                        Create(alignedPtr, xdsSize, bmpData.Scan0.ToPointer());
                        bmp.UnlockBits(bmpData);
                        Image = bmp;
                    }
                    catch (Exception ex)
                    {
                        if (bmp != null) bmp.Dispose();
                        throw new Exception("Unable to load texture.");
                    }
                }
            }
        }

        private unsafe void LoadBix(byte[] data, int width, int height, int format)
        {
            if (data != null && data.Length > 0 && width > 0 && height > 0 && format > 0)
            {
                Bitmap bmp = null;
                try
                {
                    // create a new bitmap and lock its position in memory
                    bmp = new Bitmap(width, height, PixelFormat.Format32bppArgb);
                    BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, bmp.PixelFormat);

                    // perform the conversion
                    fixed (byte* bixPtr = data)
                    {
                        ConvertToBitmap(bixPtr, data.Length, format, width, height, bmpData.Scan0.ToPointer());
                        bmp.UnlockBits(bmpData);
                        Image = bmp;
                    }
                }
                catch (Exception ex)
                {
                    if (bmp != null) bmp.Dispose();
                    throw new Exception("Unable to load texture.");
                }
            }
            else throw new ArgumentException();
        }

        public void Dispose()
        {
            if (Image != null)
            {
                Image.Dispose();
                Image = null;
            }
        }

        private uint ReverseEndian(uint val)
        {
            return (val << 24) | ((val & 0xFF00) << 8) | ((val & 0xFF0000) >> 8) | (val >> 24);
        }
        #endregion
    }
}
